﻿/*
 * Copyright 2011 Edward L. Stauff.                  Contact: <EdStauff@gmail.com>.
 * 
 * This file, "TimeSpanUtils.cs", is part of the "Stauffware.Common.Time" class library.
 * "Stauffware.Common.Time" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common.Time" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * the "Stauffware.Common.Time" sources.  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Text;
using Stauffware.Common.Core;

namespace Stauffware.Common.Time
{
	/// <summary>
	/// Utility methods for use with TimeSpan values.
	/// </summary>
	public static class TimeSpanUtils
	{
		//-----------------------------------------------------------------------------
		/// <summary>
		/// The list of TimeUnit values that represent a single time unit,
		/// in descending order of size.
		/// </summary>
		public static readonly TimeUnits[] SingleTimeUnits =
		{	TimeUnits.Years, TimeUnits.Weeks, TimeUnits.Days, 
			TimeUnits.Hours, TimeUnits.Minutes, TimeUnits.Seconds, TimeUnits.Millisecond };

		//-----------------------------------------------------------------------------
		#region TimeUnit stuff

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns a list of the long unit names represented by the given TimeUnit value.
		/// </summary>
		public static List<string> GetLongUnitNames (TimeUnits? units)
		{
			List<string> list = new List<string>();
			if (!units.HasValue)
				return list;
			foreach (TimeUnits u in LongUnitNames.Keys)
				if ((units & u) != 0)
					list.Add(LongUnitNames[u]);
			return list;
		}

		//-----------------------------------------------------------------------------

		/// <summary>
		/// Long names of units (single unit values of TimeUnit only).
		/// Each name is lower case, singular.
		/// </summary>
		public static readonly Dictionary<TimeUnits, string> LongUnitNames =
									new Dictionary<TimeUnits, string>();

		/// <summary>
		/// Short names of units (single unit values of TimeUnit only).
		/// Each name is lower case, singular.
		/// </summary>
		public static readonly Dictionary<TimeUnits, string> ShortUnitNames =
									new Dictionary<TimeUnits, string>();

		/// <summary>
		/// Names of units, both long and short (single unit values of TimeUnit only).
		/// Each name is lower case, plural.
		/// </summary>
		public static readonly Dictionary<string, TimeUnits> InputUnitNames =
									new Dictionary<string, TimeUnits>();

		/// <summary>
		/// Names of units for use in menus, radio buttons, etc.
		/// Each name is mixed case, singular, with an ampersand before the shortcut character.
		/// </summary>
		public static readonly Dictionary<TimeUnits, string> MenuUnitNames =
									new Dictionary<TimeUnits, string>();

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Returns the TimeUnit value corresponding to the given input string.
		/// If no match is found, returns TimeUnit.None.
		/// If multiple matches are found, returns a TimeUnit with multiple bits set.
		/// Case is not significant, nor is leading/trailing whitespace.
		/// </summary>

		public static TimeUnits LexUnitName (string input)
		{
			input = input.ToLower().Trim();
			TimeUnits ret = TimeUnits.None;
			if (input == "") return ret;

			foreach (string name in InputUnitNames.Keys)
				if (name.StartsWith(input))
					ret = ret | InputUnitNames[name];

			return ret;
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Converts between TimeUnits.
		/// </summary>
		/// <param name="value">the value to be converted</param>
		/// <param name="oldUnits">the units to be converted from</param>
		/// <param name="newUnits">the units to be converted to</param>
		/// <returns>the new value</returns>
		/// <remarks>
		/// Conversions between week, day, hour, minute, and second are exact.
		/// Conversions between month and year are exact.
		/// Other conversions are based on 1 year = 365.25 days,
		/// and 1 month = 1/12 year.
		/// </remarks>

		public static decimal ConvertUnits (decimal value, TimeUnits oldUnits, TimeUnits newUnits)
		{
			Require.Condition(oldUnits.HasSingleUnit());
			Require.Condition(newUnits.HasSingleUnit());
			return value * UnitConversionFactors[oldUnits][newUnits];
		}

		//-----------------------------------------------------------------------------

		// UnitConversionFactors[unitA][unitB] = N where A * N = B
		private static readonly Dictionary<TimeUnits, Dictionary<TimeUnits, decimal>> UnitConversionFactors =
				new Dictionary<TimeUnits, Dictionary<TimeUnits, decimal>>();

		//-----------------------------------------------------------------------------
		// called by class constructor

		private static void InitializeUnitConversionFactors ( )
		{
			// create the dictionaries and initialize the identity conversions
			foreach (TimeUnits u in SingleTimeUnits)
			{
				UnitConversionFactors[u] = new Dictionary<TimeUnits, decimal>();
				UnitConversionFactors[u][u] = 1M;
			}

			// initialize half of the remaining conversions

			UnitConversionFactors[TimeUnits.Years][TimeUnits.Weeks] = 365.25M / 7M;
			UnitConversionFactors[TimeUnits.Years][TimeUnits.Days] = 365.25M;
			UnitConversionFactors[TimeUnits.Years][TimeUnits.Hours] = 365.25M * 24M;
			UnitConversionFactors[TimeUnits.Years][TimeUnits.Minutes] = 365.25M * 24M * 60M;
			UnitConversionFactors[TimeUnits.Years][TimeUnits.Seconds] = 365.25M * 24M * 60M * 60M;

			UnitConversionFactors[TimeUnits.Weeks][TimeUnits.Days] = 7M;
			UnitConversionFactors[TimeUnits.Weeks][TimeUnits.Hours] = 7M * 24M;
			UnitConversionFactors[TimeUnits.Weeks][TimeUnits.Minutes] = 7M * 24M * 60M;
			UnitConversionFactors[TimeUnits.Weeks][TimeUnits.Seconds] = 7M * 24M * 60M * 60M;

			UnitConversionFactors[TimeUnits.Days][TimeUnits.Hours] = 24M;
			UnitConversionFactors[TimeUnits.Days][TimeUnits.Minutes] = 24M * 60M;
			UnitConversionFactors[TimeUnits.Days][TimeUnits.Seconds] = 24M * 60M * 60M;

			UnitConversionFactors[TimeUnits.Hours][TimeUnits.Minutes] = 60M;
			UnitConversionFactors[TimeUnits.Hours][TimeUnits.Seconds] = 60M * 60M;

			UnitConversionFactors[TimeUnits.Minutes][TimeUnits.Seconds] = 60M;

			// initialize the remaining conversions as reciprocals of
			// the ones we just did
			foreach (TimeUnits u1 in SingleTimeUnits)
				foreach (TimeUnits u2 in SingleTimeUnits)
					if (!UnitConversionFactors[u1].ContainsKey(u2))
						UnitConversionFactors[u1][u2] = 1M / UnitConversionFactors[u2][u1];
		}

		//-----------------------------------------------------------------------------

		#endregion

		#region TimeSpan stuff

		//=====================================================================
		/// <summary>
		/// Creates a TimeSpan from the given values.
		/// </summary>

		public static TimeSpan CreateTimeSpan (double value, TimeUnits units)
		{
			switch (units)
			{
				case TimeUnits.Seconds: return TimeSpan.FromSeconds(value);
				case TimeUnits.Minutes: return TimeSpan.FromMinutes(value);
				case TimeUnits.Hours: return TimeSpan.FromHours(value);
				case TimeUnits.Days: return TimeSpan.FromDays(value);
				case TimeUnits.Weeks: return TimeSpan.FromDays(value * 7);
				case TimeUnits.Years: return TimeSpan.FromDays(value * 365.25);
				default:
					Assert.NotReached("bogus TimeUnit: " + units.ToString());
					return TimeSpan.Zero;	// shut compiler up
			}
		}

		//=====================================================================
		/// <summary>
		/// Returns the total number of the given time units in the given TimeSpan.
		/// </summary>

		public static double ToUnitValue (this TimeSpan timeSpan, TimeUnits units)
		{
			switch (units)
			{
				case TimeUnits.Seconds: return timeSpan.TotalSeconds;
				case TimeUnits.Minutes: return timeSpan.TotalMinutes;
				case TimeUnits.Hours: return timeSpan.TotalHours;
				case TimeUnits.Days: return timeSpan.TotalDays;
				case TimeUnits.Weeks: return timeSpan.TotalDays / 7;
				case TimeUnits.Years: return timeSpan.TotalDays / 365.25;
				default:
					Assert.NotReached("bogus TimeUnit: " + units.ToString());
					return double.NegativeInfinity;	// shut compiler up
			}
		}

		//=====================================================================
		/// <summary>
		/// Multiplies a TimeSpan by the given amount, and returns the result.
		/// </summary>
		
		[Obsolete("Use Multiply instead (in Time_Extensions).")]
		public static TimeSpan MultiplyBy (this TimeSpan ts, double factor)
		{
			return TimeSpan.FromMilliseconds(ts.TotalMilliseconds * factor);
		}

		//=====================================================================
		/// <summary>
		/// Converts a TimeSpan to a string, using the given units.
		/// </summary>
		/// <param name="timeSpan">the value to be converted to a string</param>
		/// <param name="units">the units to be used (possibly more than one unit)</param>

		public static string ToString (TimeSpan? timeSpan, TimeUnits units)
		{
			return ToString(timeSpan, units, null);
		}

		//=====================================================================
		/// <summary>
		/// Converts a TimeSpan to a string, using the given units.
		/// </summary>
		/// <param name="nTimeSpan">the value to be converted to a string</param>
		/// <param name="units">the units to be used</param>
		/// <param name="format">
		/// Optional format string, or null.  The syntax depends on the units specified.
		/// For a single unit, the format string is passed to double.ToString().
		/// </param>
		/// <remarks>
		/// All possible TimeUnit values are supported, except for TimeUnit.None,
		/// which is only allowed for values of TimeSpan.Zero or NullTimeSpan.
		/// A timespan of Zero gives a string of "0", without units.
		/// NullTimeSpan gives an empty string.
		/// </remarks>

		public static string ToString (TimeSpan? nTimeSpan, TimeUnits units, string format)
		{
			if (!nTimeSpan.HasValue)
				return "";
			TimeSpan timeSpan = nTimeSpan.Value;

			if (timeSpan == TimeSpan.Zero)
				return "0";

			Require.Condition(units != TimeUnits.None);

			double number = 0;

			switch (units)
			{
				case TimeUnits.Seconds:
				case TimeUnits.Minutes:
				case TimeUnits.Hours:
				case TimeUnits.Days:
				case TimeUnits.Weeks:
				case TimeUnits.Years:
					number = timeSpan.ToUnitValue(units);
					break;

				case TimeUnits.Hours | TimeUnits.Minutes:
					return ((long) timeSpan.TotalHours).ToString() + ":" +
									timeSpan.Minutes.ToString("00");

				case TimeUnits.Hours | TimeUnits.Minutes | TimeUnits.Seconds:
					return ((long) timeSpan.TotalHours).ToString() + ":" +
									timeSpan.Minutes.ToString("00") + ":" +
									timeSpan.Seconds.ToString("00");

				// other combinations can be added as needed

				default:
					string retStr = "";

					foreach (TimeUnits u in SingleTimeUnits)
					{
						if ((units & u) == 0) continue;

						units &= ~u;	// mask off the bit we just found

						number = timeSpan.ToUnitValue(u);

						// If this is the last unit we can use, then keep the
						// fractional portion.  Otherwise, use a whole number.
						if (units != 0)
							number = (long) number;

						timeSpan = timeSpan - CreateTimeSpan(number, u);

						if (number == 1)
							retStr += " " + number.ToString() + " " + LongUnitNames[u];
						else if (number != 0)
							retStr += " " + number.ToString() + " " + LongUnitNames[u] + "s";

						if (timeSpan == TimeSpan.Zero)
							break;
					}

					return retStr.Substring(1);		// drop leading space
			}

			if (number == 1.0)
				return number.ToString(format) + " " + LongUnitNames[units];
			else return number.ToString(format) + " " + LongUnitNames[units] + "s";
		}

		#endregion


	}

	//#####################################################################################
	/// Extension methods

	public static class TimeUnits_Extensions
	{
		/// <summary>
		/// Returns whether the given TimeUnits value represents exactly one unit of time.
		/// </summary>
		public static bool HasSingleUnit (this TimeUnits units)
		{
			return Array.IndexOf(TimeSpanUtils.SingleTimeUnits, units) >= 0;
		}

		/// <summary>
		/// Returns the number of units specified in the given TimeUnits value;
		/// that is, the number of bits set in the enum.
		/// </summary>
		public static int CountUnits (this TimeUnits units)
		{
			int count = 0;
			for (int n = (int) units; n != 0; n = n >> 1)
				if ((n & 1) != 0)
					count++;
			return count;
		}

	}
}
